import path from 'path';
import util from 'util';
import fs from 'fs';
import moment from 'moment';
import callsite from 'callsite';
import express from 'express';
import colors from 'colors';
import morgan from 'morgan';

// cyan 蓝绿色；青色
// grey 灰色的
// magenta 品红色；洋红色
// reset 白色
colors.setTheme({
  CRI: 'red',
  WRN: 'yellow',
  INF: 'grey',
  SUC: 'green',
  DBG: 'blue',
  VBS: 'green',
  TRC: 'blue',
  PRE: 'gray'
});

let Console = console['Console'];

let conf: any = {};
conf.default = {
  logdir: path.join(__dirname, '../../logs'),
  prefix: 'logger_',
  console: true,

  file: null,
  ymd: null,
  logger: null
};

function init_mod_conf(opt: any, mod: string) {
  let c = get_log_conf(mod);
  if (opt.path)
    c.logdir = opt.path;
  if (opt.prefix)
    c.prefix = opt.prefix;
  if (opt.console != undefined)
    c.console = opt.console;

  if (!fs.existsSync(c.logdir))
    fs.mkdirSync(c.logdir);

  c.ymd = moment().format("YYMMDD");
  c.file = fs.createWriteStream(c.logdir + '/' + c.prefix + c.ymd + '.log', { flags: "a" });
  c.logger = new Console(c.file, c.file);
}

function get_log_conf(mod: string) {
  let c = conf[mod];
  if (c == undefined)
    c = conf.default;
  return c;
}

let logformat = '[%s][%s][%s](%s:%d): %s';
let consoleformat = colors["PRE"]('[%s][%s](%s:%d): ') + '%s';

function writelog(level: string, mod: string, now: any, trace: boolean | undefined, logstr: string, stack: any) {
  let c = get_log_conf(mod);
  now = moment(now);
  let ymd = now.format("YYMMDD");
  if (ymd != c.ymd) {
    c.ymd = ymd;
    if (c.file)
      c.file.end();
    c.file = fs.createWriteStream(c.logdir + '/' + c.prefix + c.ymd + '.log', { flags: "a" });
    c.logger = new Console(c.file, c.file);
  }

  let meta = getCallerStackFrame();
  let filename = meta.filePath;
  let linenumber = meta.fileLine;
  let basename = meta.fileName;

  let color = colors[level];
  let outstr = util.format(logformat, now.format('HH:mm:ss.SSSS'), level, mod, filename, linenumber, logstr);
  let consolestr = outstr;
  if (c.console && color) {
    consolestr = util.format(consoleformat, level, mod, basename, linenumber, color(logstr));
  }

  if (c.console)
    console.log(consolestr);
  c.logger.log(outstr);
  if (trace) {
    stack.forEach(function (point: any) {
      let line = util.format('    at %s', point.line);
      if (c.console)
        console.log(line);
      c.logger.log(line);
    });
  }
}

function sendlog(level: string, mod: string, now: any, trace: any, logstr: string, stack: any) {
  if (process.send) process.send({ cmd: 'log', level: level, mod: mod, now: now, trace: trace, logstr: logstr, stack: stack });
}

let outputlog = writelog;

function loggerInit(options: any) {
  if (!options) {
    options = {};
  }

  init_mod_conf(options, 'default');
  if (typeof (options.mods) == 'object') {
    for (let mod in options.mods) {
      let opt = options.mods[mod];
      conf[mod] = {
        logdir: conf.default.logdir,
        prefix: conf.default.prefix,
        console: conf.default.console,

        file: null,
        ymd: null,
        logger: null
      };
      init_mod_conf(opt, mod);
    }
  }
}

colors.setTheme({
  silly: 'rainbow',
  input: 'grey',
  verbose: 'cyan',
  prompt: 'grey',
  info: 'green',
  data: 'grey',
  help: 'cyan',
  warn: 'yellow',
  debug: 'blue',
  error: 'red'
});

function loggerHttplog(mod: string, format: string) {
  function compile(fmt: string) {
    fmt = fmt.replace(/"/g, '\\"');
    let js = '  return "' + fmt.replace(/:([-\w]{2,})(?:\[([^\]]+)\])?/g, function (_, name, arg) {
      return '"\n    + (tokens["' + name + '"](req, res, "' + arg + '") || "-") + "';
    }) + '";';
    return new Function('tokens, req, res', js);
  }

  let fmt = morgan[format] || format || morgan['default'];
  if ('function' != typeof fmt) fmt = compile(fmt);

  //req: express.Request, res: express.Response, next: Function
  return function (req: any, res: express.Response, next: Function) {
    let sock = req.socket;
    req['_startTime'] = new Date;
    req['_remoteAddress'] = req.ip
      || req['_remoteAddress']
      || (req.connection && req.connection.remoteAddress)
      || undefined;

    function logRequest() {
      res.removeListener('finish', logRequest);
      res.removeListener('close', logRequest);
      let logstr = fmt(morgan, req, res);
      if (null == logstr)
        return;
      log('INF', mod, logstr);
    }

    res.on('finish', logRequest);
    res.on('close', logRequest);

    next();
  };
}

function loggerFinish() {
  for (let mod in conf) {
    let c = conf[mod];
    c.file.end();
    c.file = null;
    c.ymd = null;
    c.logger = null;
  }
}

function log(level: string, mod: string, logstr: string, trace?: boolean) {
  if (logstr.length == 0) {
    return;
  }
  let now = new Date();
  let callsites = callsite();

  callsites = callsites.slice(2, trace ? -1 : 3);

  let stack = callsites.map(function (point) {
    return {
      filename: point.getFileName(),
      linenumber: point.getLineNumber(),
      line: point.toString()
    };
  });
  outputlog(level, mod, now, trace, logstr, stack);
}

interface IStackFrame {
  fullFilePath?: string;
  fileName?: string;
  fileNameWithLine?: string;
  filePath?: string;
  fileLine?: string;
  fileColumn?: string;
  filePathWithLine?: string;
  method?: string;
}

function getCallerStackFrame(stackDepthLevel: number = 4, error: Error = Error()): IStackFrame {
  return stackLineToStackFrame((error as Error | undefined)?.stack?.split("\n")?.filter((thisLine: string) => thisLine.includes("    at "))?.[stackDepthLevel]);
}

function stackLineToStackFrame(line?: string): IStackFrame {
  const pathResult: IStackFrame = {
    fullFilePath: undefined,
    fileName: undefined,
    fileNameWithLine: undefined,
    fileColumn: undefined,
    fileLine: undefined,
    filePath: undefined,
    filePathWithLine: undefined,
    method: undefined,
  };
  if (line != null && line.includes("    at ")) {
    line = line.replace(/^\s+at\s+/gm, "");
    const errorStackLine = line.split(" (");
    const fullFilePath = line?.slice(-1) === ")" ? line?.match(/\(([^)]+)\)/)?.[1] : line;
    const pathArray = fullFilePath?.includes(":") ? fullFilePath?.replace("file://", "")?.replace(process.cwd(), "")?.split(":") : undefined;
    // order plays a role, runs from the back: column, line, path
    const fileColumn = pathArray?.pop();
    const fileLine = pathArray?.pop();
    const filePath = pathArray?.pop();
    const filePathWithLine = path.normalize(`${filePath}:${fileLine}`);
    const fileName = filePath?.split("/")?.pop();
    const fileNameWithLine = `${fileName}:${fileLine}`;

    if (filePath != null && filePath.length > 0) {
      pathResult.fullFilePath = fullFilePath;
      pathResult.fileName = fileName;
      pathResult.fileNameWithLine = fileNameWithLine;
      pathResult.fileColumn = fileColumn;
      pathResult.fileLine = fileLine;
      pathResult.filePath = filePath;
      pathResult.filePathWithLine = filePathWithLine;
      pathResult.method = errorStackLine?.[1] != null ? errorStackLine?.[0] : undefined;
    }
  }
  return pathResult;
}

/**
 * 记录日志类
 *
 * ```
 *  let Logger  = require("common/logger");
 *  let logger = new Logger('mod');
 *  logger.info("test");
 *  logger.error("test");
 *  logger.warn("test");
 * ```
 *
 * @param mod
 */
class Logger {
  public mod!: string;
  constructor(mod: string) {
    if (this == undefined) {
      return new Logger(mod);
    }
    if (mod == undefined) mod = '';
    this.mod = mod;
  }

  error(...args: Array<any>) {
    log('CRI', this.mod, util.format.apply(util, args));
  };

  cri = this.error;

  warn(...args: Array<any>) {
    log('WRN', this.mod, util.format.apply(util, args));
  };

  wrn = this.warn;

  info(...args: Array<any>) {
    log('INF', this.mod, util.format.apply(util, args));
  };

  inf = this.info;
  log = this.info;

  debug(...args: Array<any>) {
    log('DBG', this.mod, util.format.apply(util, args));
  };

  verbose(...args: Array<any>) {
    log('VBS', this.mod, util.format.apply(util, args));
  };
  
  success = this.verbose;

  dbg = this.debug;

  trace(...args: Array<any>) {
    log('TRC', this.mod, util.format.apply(util, args), true);
  };

  trc = this.trace;

  static init = loggerInit;
  static finish = loggerFinish;
  // static httplog = loggerHttplog;
}

export default Logger;